import sys
import shutil
import subprocess
import tempfile
import ctypes
import time
import hashlib
import zipfile
import os
import signal
from pkg_resources import parse_version
from pathlib import Path
from typing import Tuple, Optional, List
import threading
import requests
import customtkinter as ctk
from queue import Queue, Empty
from tkinter import messagebox
import pefile  # 需安装：pip install pefile

# ================== 配置区 ==================
UPDATE_CONFIG = {
    "version_url": "http://115.120.195.74/latest.json",  # 远程版本配置地址
    "timeout": 8,
}

# 路径初始化 - 修复为安装目录下的临时文件夹（解决问题1）
APP_ROOT = Path(__file__).parent if not getattr(sys, 'frozen', False) else Path(sys.executable).parent
CURRENT_EXE = Path(sys.executable).resolve()

# 创建安装目录下的update_temp文件夹（非系统盘）
UPDATE_TEMP_DIR = APP_ROOT / "update_temp"
UPDATE_TEMP_DIR.mkdir(exist_ok=True)  # 确保目录存在
TEMP_ZIP_FILE = UPDATE_TEMP_DIR / f"update_{CURRENT_EXE.stem}.zip"
TEMP_UNZIP_DIR = UPDATE_TEMP_DIR / f"update_{CURRENT_EXE.stem}_temp"

# ================== 日志初始化 ==================
try:
    from logger import app_logger as logger
except ImportError:
    import logging

    log_dir = APP_ROOT / "logs"
    log_dir.mkdir(exist_ok=True)
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler(log_dir / "app.log", encoding='utf-8'),
            logging.StreamHandler(sys.stdout)
        ]
    )
    logger = logging.getLogger("App")


# ================== 共享工具函数 ==================
def verify_sha256(file_path: Path, expected_hash: str) -> bool:
    """校验文件 SHA256 是否匹配预期值"""
    if not file_path.exists():
        logger.error(f"[校验] 文件不存在：{file_path}")
        return False
    sha256 = hashlib.sha256()
    try:
        with open(file_path, 'rb') as f:
            for chunk in iter(lambda: f.read(8192), b""):
                sha256.update(chunk)
        actual = sha256.hexdigest().lower()
        expected = expected_hash.lower()
        logger.info(f"[校验] 实际 SHA256: {actual}")
        logger.info(f"[校验] 期望 SHA256: {expected}")
        return actual == expected
    except Exception as e:
        logger.error(f"[校验] 校验失败: {e}", exc_info=True)
        return False


def is_64bit_file(file_path: Path) -> Optional[bool]:
    """判断文件是否为64位可执行文件"""
    logger.info(f"[架构识别] 检测文件架构：{file_path.name}（路径：{file_path}）")

    if not file_path.exists():
        logger.error(f"[架构识别] 文件不存在：{file_path}")
        return None
    if not file_path.is_file():
        logger.error(f"[架构识别] 不是文件：{file_path}")
        return None

    try:
        pe = pefile.PE(str(file_path))
        machine_type = pe.FILE_HEADER.Machine
        if machine_type == pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_AMD64"]:
            logger.info(f"[架构识别] 识别为64位文件")
            return True
        elif machine_type == pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_I386"]:
            logger.info(f"[架构识别] 识别为32位文件")
            return False
        else:
            logger.warning(f"[架构识别] 未知架构（Machine类型：{machine_type}）")
            return None
    except pefile.PEFormatError as e:
        logger.error(f"[架构识别] 非PE格式文件：{e}")
        return None
    except Exception as e:
        logger.error(f"[架构识别] 解析失败：{e}", exc_info=True)
        return None


def is_64bit_os() -> bool:
    """判断当前系统是否为64位"""
    result = sys.maxsize > 2 ** 32
    logger.info(f"[系统信息] 检测到{'64位' if result else '32位'}操作系统")
    return result


def is_admin() -> bool:
    """检查当前进程是否拥有管理员权限"""
    try:
        result = ctypes.windll.shell32.IsUserAnAdmin()
        logger.info(f"[权限检查] 管理员权限：{'是' if result else '否'}")
        return result
    except:
        logger.warning("[权限检查] 权限检查失败，默认返回非管理员")
        return False


# ================== 更新器UI（解决问题2：显示更新进度） ==================
class UpdaterUI:
    """更新器图形界面，显示文件复制进度和完成状态"""
    def __init__(self, source_exe: Path, target_exe: Path):
        self.root = ctk.CTk()
        self.root.title("正在更新程序...")
        self.root.geometry("400x200")
        self.root.resizable(False, False)
        self.root.attributes("-topmost", True)  # 置顶显示
        self.root.protocol("WM_DELETE_WINDOW", self._on_close)  # 禁止关闭窗口

        # 标题
        ctk.CTkLabel(
            self.root,
            text="程序更新中",
            font=("Microsoft YaHei", 16, "bold")
        ).pack(pady=10)

        # 进度条
        self.progress_var = ctk.DoubleVar(value=0)
        self.progress_bar = ctk.CTkProgressBar(
            self.root,
            variable=self.progress_var,
            mode="determinate"
        )
        self.progress_bar.pack(padx=20, fill="x")

        # 状态文本
        self.status_var = ctk.StringVar(value=f"准备替换文件：{target_exe.name}")
        ctk.CTkLabel(
            self.root,
            textvariable=self.status_var,
            font=("Microsoft YaHei", 10)
        ).pack(pady=15)

        # 进度队列（接收主逻辑的进度更新）
        self.progress_queue = Queue()
        self.update_complete = False

        # 启动UI更新线程
        self.ui_thread = threading.Thread(target=self._update_ui_loop, daemon=True)
        self.ui_thread.start()

    def _update_ui_loop(self):
        """循环更新UI进度"""
        while not self.update_complete:
            try:
                # 从队列获取进度（1-100）
                progress, status = self.progress_queue.get(timeout=0.1)
                self.progress_var.set(progress / 100)
                self.status_var.set(status)
                self.progress_queue.task_done()
            except Empty:
                continue
        # 所有更新完成后显示成功信息
        self.status_var.set("更新完成！即将启动程序...")
        self.progress_var.set(1.0)
        # 等待2秒后关闭
        self.root.after(2000, self.root.destroy)

    def update_progress(self, progress: int, status: str):
        """外部调用：更新进度"""
        self.progress_queue.put((progress, status))

    def _on_close(self):
        """禁止用户关闭窗口"""
        messagebox.showinfo("提示", "更新正在进行中，请勿关闭窗口")

    def start(self):
        """启动UI主循环"""
        self.root.mainloop()

    def set_complete(self):
        """标记更新完成"""
        self.update_complete = True


# ================== 更新器核心逻辑 ==================
def replace_files_and_restart(source_exe: Path, target_exe: Path, version_file: Path):
    """替换文件并重启程序（由updater调用）"""
    try:
        # 初始化更新器UI（解决问题2）
        updater_ui = UpdaterUI(source_exe, target_exe)
        ui_thread = threading.Thread(target=updater_ui.start, daemon=True)
        ui_thread.start()

        # 1. 等待主程序退出（最多等待10秒）
        wait_time = 0
        while is_process_running(target_exe.name) and wait_time < 10:
            updater_ui.update_progress(10, f"等待程序关闭...（{wait_time}s）")
            time.sleep(1)
            wait_time += 1
        if is_process_running(target_exe.name):
            raise Exception("主程序无法关闭，请手动关闭后重试")

        # 2. 复制主程序文件（带进度）
        updater_ui.update_progress(30, "正在替换主程序...")
        copy_with_progress(
            source_exe,
            target_exe,
            lambda p: updater_ui.update_progress(30 + p//2, f"复制文件：{p}%")
        )

        # 3. 复制版本文件
        updater_ui.update_progress(80, "正在更新版本信息...")
        shutil.copy2(version_file, APP_ROOT / "version.txt")

        # 4. 清理临时文件
        updater_ui.update_progress(90, "正在清理临时文件...")
        if TEMP_UNZIP_DIR.exists():
            shutil.rmtree(TEMP_UNZIP_DIR, ignore_errors=True)
        if TEMP_ZIP_FILE.exists():
            TEMP_ZIP_FILE.unlink(missing_ok=True)

        # 5. 标记完成并重启程序
        updater_ui.update_progress(100, "更新完成，准备启动程序...")
        updater_ui.set_complete()
        time.sleep(1)  # 等待UI显示完成信息

        # 启动新版本程序
        subprocess.Popen(str(target_exe), cwd=str(APP_ROOT))
        logger.info("更新完成，已启动新版本程序")

    except Exception as e:
        logger.error(f"更新失败: {e}", exc_info=True)
        messagebox.showerror("更新失败", f"替换文件时出错：{str(e)}\n请手动更新或联系技术支持")
        sys.exit(1)


def is_process_running(exe_name: str) -> bool:
    """检查进程是否在运行"""
    try:
        import psutil
        for proc in psutil.process_iter(['name']):
            if proc.info['name'] == exe_name:
                return True
        return False
    except ImportError:
        # 备选方案：通过tasklist检查
        output = subprocess.check_output(
            f'tasklist /FI "IMAGENAME eq {exe_name}"',
            shell=True,
            text=True
        )
        return exe_name in output
    except Exception as e:
        logger.warning(f"检查进程失败: {e}")
        return False


def copy_with_progress(src: Path, dst: Path, progress_callback):
    """带进度的文件复制"""
    chunk_size = 1024 * 1024  # 1MB块
    total_size = os.path.getsize(src)
    copied = 0

    with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst:
        while True:
            chunk = fsrc.read(chunk_size)
            if not chunk:
                break
            fdst.write(chunk)
            copied += len(chunk)
            progress = int((copied / total_size) * 50)  # 0-50的进度值
            progress_callback(progress)


# ================== 启动独立更新器 ==================
def launch_updater(extracted_exe: Path, current_executable: Path, expected_sha256: str):
    """启动独立 updater 进程并安全退出主程序"""
    try:
        logger.info(f"[更新器] 准备启动独立更新器，待安装文件：{extracted_exe}，目标文件：{current_executable}")

        # 定位更新器执行文件
        if getattr(sys, 'frozen', False):
            updater_executable = APP_ROOT / "updater.exe"
            if not updater_executable.exists():
                raise FileNotFoundError(f"未找到 updater.exe: {updater_executable}")
            # 为路径添加引号（处理空格）
            cmd = [
                f'"{updater_executable}"',
                f'"{extracted_exe}"',
                f'"{current_executable}"',
                expected_sha256
            ]
        else:
            updater_script = APP_ROOT / "updater.py"
            if not updater_script.exists():
                raise FileNotFoundError(f"未找到 updater.py: {updater_script}")
            # 为路径添加引号（处理空格）
            cmd = [
                f'"{sys.executable}"',
                f'"{updater_script}"',
                f'"{extracted_exe}"',
                f'"{current_executable}"',
                expected_sha256
            ]

        logger.info(f"[更新器] 启动命令：{' '.join(cmd)}")

        # 启动更新器（确保以管理员权限启动）
        subprocess.Popen(
            ' '.join(cmd),  # 作为整体命令字符串执行（支持引号）
            cwd=str(APP_ROOT),
            shell=True,  # 启用shell解析带引号的路径
            close_fds=True,
            creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
        )

        # 安全退出主程序
        logger.info("[更新器] 主程序准备退出，释放资源...")
        for thread in threading.enumerate():
            if thread != threading.main_thread():
                try:
                    thread.join(timeout=1.0)
                except Exception as e:
                    logger.warning(f"[退出] 线程 {thread.name} 关闭失败: {e}")

        # 强制终止进程
        if os.name == "nt":
            os.kill(os.getpid(), signal.SIGTERM)
        else:
            os.kill(os.getpid(), signal.SIGKILL)
        sys.exit(0)

    except Exception as e:
        logger.error(f"[更新器] 启动 updater 失败: {e}", exc_info=True)
        messagebox.showerror("更新错误", f"无法启动更新器：{e}")
        raise


# ================== 压缩包处理工具函数 ==================
def extract_zip(zip_path: Path, extract_dir: Path) -> List[Path]:
    """解压zip文件到指定目录，返回解压后的文件列表"""
    try:
        if extract_dir.exists():
            shutil.rmtree(extract_dir)  # 清理旧解压目录
        extract_dir.mkdir(exist_ok=True)

        with zipfile.ZipFile(zip_path, 'r') as zip_ref:
            zip_ref.extractall(extract_dir)
            extracted_files = [extract_dir / f for f in zip_ref.namelist()]
            logger.info(f"[解压] 成功解压 {len(extracted_files)} 个文件到 {extract_dir}")
            return extracted_files
    except zipfile.BadZipFile:
        raise Exception(f"压缩包损坏：{zip_path}")
    except Exception as e:
        logger.error(f"[解压] 解压失败: {e}", exc_info=True)
        raise


def find_main_exe_in_extracted(extract_dir: Path) -> Optional[Path]:
    """从解压目录中查找主程序EXE（与当前执行文件名一致）"""
    target_exe_name = CURRENT_EXE.name
    for item in extract_dir.rglob("*"):
        if item.is_file() and item.name == target_exe_name and item.suffix == ".exe":
            logger.info(f"[解压] 找到主程序：{item}")
            return item
    logger.error(f"[解压] 未在 {extract_dir} 中找到 {target_exe_name}")
    return None


# ================== 更新流程（下载 + 解压 + 校验） ==================
# ================== 更新流程（下载 + 解压 + 校验） ==================
def perform_update_flow(current_ver: str, remote_ver: str, download_url: str, changelog: str, expected_sha256: str,
                        remote_arch: str) -> bool:
    # 1. 系统位数与更新包架构兼容性校验
    logger.info(f"[更新流程] 开始架构兼容性校验")
    is_64 = is_64bit_os()
    logger.info(f"[更新流程] 本地系统为{'64位' if is_64 else '32位'}，远程包架构为{remote_arch}")

    if is_64 and remote_arch not in ("x64", "64"):
        messagebox.showerror("架构不兼容", f"当前系统为64位，但更新包为{remote_arch}位，无法更新！")
        logger.error(f"[更新流程] 架构不兼容")
        return False
    if not is_64 and remote_arch not in ("x86", "32"):
        messagebox.showerror("架构不兼容", f"当前系统为32位，但更新包为{remote_arch}位，无法更新！")
        logger.error(f"[更新流程] 架构不兼容")
        return False

    # 2. 管理员权限校验
    if not is_admin():
        messagebox.showerror("权限不足", "更新需要管理员权限。\n请右键程序，选择“以管理员身份运行”后再试。")
        logger.warning("[更新流程] 无管理员权限，更新终止")
        return False

    # 3. 创建更新窗口
    updater_win = ctk.CTk()
    updater_win.title("正在更新...")
    updater_win.geometry("500x200")
    updater_win.resizable(False, False)
    updater_win.attributes("-topmost", True)

    status_label = ctk.CTkLabel(updater_win, text="准备中...", font=("Microsoft YaHei", 12))
    status_label.pack(pady=5)

    progress_bar = ctk.CTkProgressBar(updater_win, mode="determinate")
    progress_bar.set(0.0)
    progress_bar.pack(pady=5, padx=20, fill="x")

    speed_label = ctk.CTkLabel(updater_win, text="速度: -- KB/s", font=("Microsoft YaHei", 10), text_color="gray")
    speed_label.pack()

    cancel_event = threading.Event()
    result_queue = Queue()
    update_queue = Queue()  # 子线程向主线程发送UI更新指令

    def background_update_flow():
        """后台更新流程：下载→校验→解压→架构校验→等待更新器完成后再更新版本"""
        try:
            # 清理旧临时文件
            if TEMP_ZIP_FILE.exists():
                TEMP_ZIP_FILE.unlink()
            if TEMP_UNZIP_DIR.exists():
                shutil.rmtree(TEMP_UNZIP_DIR)

            # 下载压缩包
            logger.info(f"[更新] 开始下载新版本压缩包: {download_url}")
            update_queue.put(("status", "正在连接服务器..."))

            resp = requests.get(download_url, stream=True, timeout=30)
            resp.raise_for_status()
            total = int(resp.headers.get('content-length', 0))
            downloaded = 0
            last_time = time.time()
            last_downloaded = 0

            with open(TEMP_ZIP_FILE, 'wb') as f:
                for chunk in resp.iter_content(chunk_size=8192):
                    if cancel_event.is_set():
                        logger.info("[更新] 用户取消更新")
                        if TEMP_ZIP_FILE.exists():
                            TEMP_ZIP_FILE.unlink()
                        raise Exception("用户取消更新")
                    if chunk:
                        f.write(chunk)
                        downloaded += len(chunk)

                        # 计算下载速度并更新UI
                        now = time.time()
                        elapsed = now - last_time
                        if elapsed > 0.5:
                            speed_kb = (downloaded - last_downloaded) / elapsed / 1024
                            speed_text = f"速度: {speed_kb:.1f} KB/s | 已下载: {downloaded // 1024} KB"
                            update_queue.put(("speed", speed_text))
                            last_time = now
                            last_downloaded = downloaded

                        # 更新进度条（下载占70%进度）
                        if total > 0:
                            progress = min(0.7, downloaded / total * 0.7)
                            update_queue.put(("progress", progress))
                            update_queue.put(("status", "正在下载新版本..."))

            logger.info(f"[更新] 下载完成: {TEMP_ZIP_FILE}")
            update_queue.put(("status", "正在解压更新包..."))
            update_queue.put(("progress", 0.7))

            # 解压压缩包
            extracted_files = extract_zip(TEMP_ZIP_FILE, TEMP_UNZIP_DIR)
            if not extracted_files:
                raise Exception("压缩包内无文件，更新失败")

            # 查找主程序EXE
            update_queue.put(("status", "正在识别更新文件..."))
            update_queue.put(("progress", 0.8))
            main_exe = find_main_exe_in_extracted(TEMP_UNZIP_DIR)
            if not main_exe:
                raise Exception("未在更新包中找到主程序文件")

            # 查找更新包中的version.txt（暂不替换，等待更新器完成）
            extracted_ver_file = TEMP_UNZIP_DIR / "version.txt"
            if not extracted_ver_file.exists():
                raise Exception("更新包中未找到 version.txt，无法更新版本信息")
            logger.info(f"[更新] 找到新版本文件：{extracted_ver_file}")

            # 校验主程序SHA256
            update_queue.put(("status", "正在校验主程序完整性..."))
            update_queue.put(("progress", 0.85))
            if not verify_sha256(main_exe, expected_sha256):
                raise Exception("主程序SHA256校验失败，文件可能被篡改")

            # 校验主程序架构
            update_queue.put(("status", "正在校验文件架构..."))
            update_queue.put(("progress", 0.9))
            is_64bit = is_64bit_file(main_exe)
            if is_64bit is None:
                raise Exception(f"无法识别主程序架构: {main_exe.name}")

            # 系统与主程序架构兼容性校验
            if is_64bit_os() and not is_64bit:
                raise Exception(f"主程序为32位，与64位系统不兼容")
            if not is_64bit_os() and is_64bit:
                raise Exception(f"主程序为64位，与32位系统不兼容")

            # 准备启动更新器（此时不更新版本文件，由更新器完成替换后更新）
            logger.info("[更新] 所有校验通过，准备启动更新器...")
            update_queue.put(("status", "校验通过，准备更新..."))
            update_queue.put(("progress", 0.95))

            # 将新版本文件路径传递给更新器，由更新器完成替换后再更新版本号
            result_queue.put(("ready_to_update", main_exe, extracted_ver_file, expected_sha256))

        except Exception as e:
            error_msg = str(e)
            logger.error(f"[更新] 流程失败: {error_msg}", exc_info=True)
            result_queue.put(("error", error_msg))
        finally:
            # 清理临时压缩包
            if TEMP_ZIP_FILE.exists():
                try:
                    TEMP_ZIP_FILE.unlink()
                    logger.info(f"[更新] 清理临时压缩包: {TEMP_ZIP_FILE}")
                except Exception as e:
                    logger.warning(f"[更新] 清理临时文件失败: {e}")

    # 启动更新线程
    thread = threading.Thread(target=background_update_flow, daemon=True)
    thread.start()

    def on_cancel():
        cancel_event.set()
        cancel_button.configure(state="disabled")
        status_label.configure(text="正在取消...")
        logger.info("[更新] 用户点击取消更新")

    cancel_button = ctk.CTkButton(
        updater_win, text="取消更新", command=on_cancel,
        fg_color="red", hover_color="#A00", width=100
    )
    cancel_button.pack(pady=10)

    # 主线程轮询队列更新UI
    def process_ui_updates():
        while True:
            try:
                msg_type, payload = update_queue.get_nowait()
                if msg_type == "status":
                    status_label.configure(text=payload)
                elif msg_type == "progress":
                    progress_bar.set(payload)
                elif msg_type == "speed":
                    speed_label.configure(text=payload)
            except Empty:
                break

        if not result_queue.empty():
            msg_type, *payload = result_queue.get()
            if msg_type == "ready_to_update":
                main_exe, extracted_ver_file, sha256 = payload
                updater_win.destroy()
                # ✅ 关键修改：不再调用 launch_updater_with_ver_file
                # 而是让主程序自己退出并启动 updater
                exit_and_launch_updater(main_exe, CURRENT_EXE, extracted_ver_file, sha256)
                return
            else:
                messagebox.showerror("更新失败", payload[0])
                updater_win.destroy()
                # 清理解压目录
                if TEMP_UNZIP_DIR.exists():
                    try:
                        shutil.rmtree(TEMP_UNZIP_DIR)
                    except Exception as e:
                        logger.warning(f"[更新] 取消后清理解压目录失败: {e}")
                return

        if thread.is_alive():
            updater_win.after(100, process_ui_updates)
        else:
            updater_win.destroy()

    updater_win.after(100, process_ui_updates)
    updater_win.mainloop()

def kill_process_by_name(process_name: str) -> bool:
    """通过进程名强制关闭程序（解决文件占用）"""
    """强制终止所有指定名称的进程（Windows 示例）"""
    try:
        import psutil
        current_pid = os.getpid()
        logger.info(f"[退出流程] 当前进程 PID: {os.getpid()}---{CURRENT_EXE.process_name}")
        for proc in psutil.process_iter(['pid', 'name']):
            try:
                if proc.info['name'] == process_name and proc.info['pid'] != current_pid:
                    proc.kill()
                    proc.wait(timeout=3)
                    logger.info(f"[退出流程] 已强制终止同名进程 PID {proc.info['pid']}")
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.TimeoutExpired):
                pass
    except ImportError:
        logger.warning("[退出流程] psutil 未安装，跳过同名进程清理")
    except Exception as e:
        logger.error(f"[退出流程] kill_process_by_name 异常: {e}")

'''
def launch_updater_with_ver_file(extracted_exe: Path, current_executable: Path, extracted_ver_file: Path, expected_sha256: str):
    """增强版启动更新器函数，传递版本文件路径"""
    try:
        logger.info(f"[更新器] 准备启动更新器（含版本文件）：{extracted_ver_file}")

        if getattr(sys, 'frozen', False):
            updater_executable = APP_ROOT / "updater.exe"
            if not updater_executable.exists():
                raise FileNotFoundError(f"未找到 updater.exe: {updater_executable}")
            # 为所有路径参数添加英文引号（处理空格）
            cmd = [
                f'"{updater_executable}"',
                f'"{extracted_exe}"',
                f'"{current_executable}"',
                expected_sha256,
                f'"{extracted_ver_file}"'
            ]
        else:
            updater_script = APP_ROOT / "updater.py"
            if not updater_script.exists():
                raise FileNotFoundError(f"未找到 updater.py: {updater_script}")
            # 为所有路径参数添加英文引号（处理空格）
            cmd = [
                f'"{sys.executable}"',
                f'"{updater_script}"',
                f'"{extracted_exe}"',
                f'"{current_executable}"',
                expected_sha256,
                f'"{extracted_ver_file}"'
            ]

        # 将命令列表拼接为单个字符串（确保引号被正确解析）
        cmd_str = ' '.join(cmd)
        logger.info(f"[更新器] 启动命令：{cmd_str}")
        logger.info(f"[更新器核心] 开始执行更新器启动逻辑")

        # 强制关闭主程序进程（通过进程名精准匹配）
        main_exe_name = os.path.basename(current_executable)
        if kill_process_by_name(main_exe_name):  # 新增函数，见下方
            logger.info(f"已强制关闭主程序进程：{main_exe_name}")
        else:
            logger.warning("未检测到主程序进程，直接启动更新器")

        # 等待1秒确保资源释放
        time.sleep(1)

        # 启动更新器（强制以管理员权限启动）
        try:
            # 使用ShellExecuteW时传递完整命令字符串，并检查返回值
            result = ctypes.windll.shell32.ShellExecuteW(
                None, "runas", cmd[0].strip('"'),  # 移除路径的引号（ShellExecuteW第一个参数需纯路径）
                ' '.join(cmd[1:]),  # 传递剩余参数（已包含引号）
                None,
                1  # 1=SW_SHOWNORMAL（显示窗口，便于调试）
            )
            # 检查ShellExecuteW返回值（小于32表示失败）
            if result <= 32:
                error_codes = {
                    2: "文件未找到（可能updater.exe缺失）",
                    5: "权限被拒绝（用户未允许UAC请求）",
                    11: "文件格式错误",
                    1223: "用户取消了操作"
                }
                err_msg = error_codes.get(result, f"未知错误（代码：{result}）")
                logger.error(f"[更新器核心] ShellExecuteW启动失败：{err_msg}")
                messagebox.showerror("更新启动失败", f"无法启动更新器：{err_msg}\n请手动运行 updater.exe")
                return
            logger.info(f"[更新器核心] 已通过ShellExecuteW发送管理员权限启动请求（返回值：{result}）")
        except Exception as e:
            # 备选方案：直接启动（可能因权限失败）
            logger.warning(f"[更新器] 权限提升失败，尝试直接启动: {e}")
            subprocess.Popen(
                cmd_str,  # 传递带引号的完整命令
                shell=True,  # 启用shell解析引号
                cwd=str(APP_ROOT),
                close_fds=True,
                creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
            )

        # 退出主程序逻辑
        logger.info("[更新器核心] 主程序开始退出流程")
        for thread in threading.enumerate():
            if thread != threading.main_thread():
                try:
                    thread.join(timeout=1.0)
                except Exception as e:
                    logger.warning(f"[退出] 线程 {thread.name} 关闭失败: {e}")

        # 强制终止进程确保资源释放
        if os.name == "nt":
            os.kill(os.getpid(), signal.SIGTERM)
        else:
            os.kill(os.getpid(), signal.SIGKILL)
        sys.exit(0)

    except Exception as e:
        logger.error(f"[更新器核心] 启动失败: {e}", exc_info=True)
        messagebox.showerror("更新错误", f"无法启动更新器：{e}")
        raise
'''

# ================== 工具函数 ==================
def clean_old_mei_tempdirs():
    """清理PyInstaller残留的临时目录"""
    try:
        temp_dir = Path(tempfile.gettempdir())
        logger.info(f"[清理] 开始清理临时目录：{temp_dir}")
        count = 0
        for item in temp_dir.iterdir():
            if item.is_dir() and item.name.startswith("_MEI"):
                if hasattr(sys, '_MEIPASS') and str(item) == sys._MEIPASS:
                    continue
                try:
                    shutil.rmtree(item)
                    logger.info(f"[清理] 已删除残留临时目录: {item}")
                    count += 1
                except PermissionError:
                    logger.warning(f"[清理] 无权限删除目录: {item}")
                except Exception as e:
                    logger.debug(f"[清理] 清理 {item} 时出错: {e}")
        logger.info(f"[清理] 临时目录清理完成，共删除 {count} 个目录")
    except Exception as e:
        logger.warning(f"[清理] 清理临时目录异常: {e}")


def resource_path(relative_path: str) -> Path:
    """获取资源文件的绝对路径（兼容开发/打包环境）"""
    try:
        base_path = Path(sys._MEIPASS)
        logger.info(f"[资源路径] 使用打包后的资源路径：{base_path}")
    except AttributeError:
        base_path = Path(__file__).parent
        logger.info(f"[资源路径] 使用开发环境资源路径：{base_path}")
    return base_path / relative_path


def get_current_version() -> str:
    """获取当前程序版本号 —— 从安装目录读取，而非打包资源"""
    # 关键修改：直接读 APP_ROOT/version.txt
    ver_file = APP_ROOT / "version.txt"
    logger.info(f"[版本获取] 读取安装目录版本文件：{ver_file}")
    if ver_file.exists():
        try:
            with open(ver_file, 'r', encoding='utf-8') as f:
                ver = f.read().strip()
                logger.info(f"[版本获取] 安装目录版本号: {ver}")
                return ver
        except Exception as e:
            logger.error(f"[版本获取] 读取 {ver_file} 失败: {e}", exc_info=True)
    else:
        logger.warning(f"[版本获取] 安装目录版本文件不存在：{ver_file}，回退到默认 1.0.0")
    return "1.0.0"


def get_remote_version() -> Tuple[Optional[str], Optional[str], str, Optional[str], Optional[str]]:
    """获取远程版本信息"""
    try:
        logger.info(f"[远程版本] 请求远程版本信息: {UPDATE_CONFIG['version_url']}")
        resp = requests.get(UPDATE_CONFIG["version_url"], timeout=UPDATE_CONFIG["timeout"], allow_redirects=False)
        if 300 <= resp.status_code < 400:
            logger.warning(f"[远程版本] 服务器返回重定向: {resp.status_code}")
            raise Exception("服务器重定向，不支持")
        resp.raise_for_status()
        data = resp.json()
        version = data.get("version")
        url = data.get("url")
        changelog = data.get("changelog", "")
        exe_sha256 = data.get("exe_sha256")
        arch = data.get("arch")
        logger.info(f"[远程版本] 远程版本号: {version}，下载地址: {url[:50]}...，架构: {arch}")
        return version, url, changelog, exe_sha256, arch
    except requests.exceptions.Timeout:
        logger.error("[远程版本] 请求超时")
        return None, None, "", None, None
    except requests.exceptions.ConnectionError as e:
        logger.error(f"[远程版本] 连接失败: {e}")
        return None, None, "", None, None
    except Exception as e:
        logger.error(f"[远程版本] 获取失败: {e}", exc_info=True)
        return None, None, "", None, None


# ================== 更新提示窗口 ==================
def show_update_dialog(current_ver: str, remote_ver: str, changelog: str) -> bool:
    """显示更新提示对话框"""
    logger.info(f"[更新提示] 显示更新对话框（当前版本：{current_ver}，新版本：{remote_ver}）")
    root = ctk.CTk()
    root.title("发现新版本")
    root.geometry("520x460")
    root.resizable(False, False)
    root.attributes("-topmost", True)

    def on_closing():
        pass

    root.protocol("WM_DELETE_WINDOW", on_closing)

    main_frame = ctk.CTkFrame(root)
    main_frame.pack(fill="both", expand=True, padx=15, pady=15)

    title_label = ctk.CTkLabel(main_frame, text="🎉 发现新版本！", font=("Microsoft YaHei", 18, "bold"))
    title_label.pack(pady=(10, 5))

    version_label = ctk.CTkLabel(main_frame, text=f"当前版本：{current_ver}   →   新版本：{remote_ver}")
    version_label.pack()

    ctk.CTkLabel(main_frame, text="更新内容：", font=("Microsoft YaHei", 12, "underline"), anchor="w").pack(anchor="w",
                                                                                                           padx=10,
                                                                                                           pady=(15, 5))

    text_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
    text_frame.pack(fill="both", expand=True, padx=10, pady=(0, 15))

    textbox = ctk.CTkTextbox(text_frame, wrap="word", font=("Microsoft YaHei", 11), corner_radius=6)
    textbox.pack(side="left", fill="both", expand=True)
    textbox.insert("0.0", changelog if changelog else "暂无更新说明。")
    textbox.configure(state="disabled")

    scrollbar = ctk.CTkScrollbar(text_frame, command=textbox.yview)
    scrollbar.pack(side="right", fill="y")
    textbox.configure(yscrollcommand=scrollbar.set)

    result = {"update_now": False}

    def on_update():
        result["update_now"] = True
        root.destroy()
        logger.info("[更新提示] 用户选择立即更新")

    def on_later():
        result["update_now"] = False
        root.destroy()
        logger.info("[更新提示] 用户选择稍后更新")

    button_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
    button_frame.pack(fill="x", pady=(0, 10))

    ctk.CTkButton(button_frame, text="立即更新", command=on_update, fg_color="green", hover_color="#2E8B57",
                  width=120).pack(side="left", padx=(0, 15))
    ctk.CTkButton(button_frame, text="稍后再说", command=on_later, fg_color="gray", hover_color="#A9A9A9",
                  width=120).pack(side="right")

    root.grab_set()
    root.mainloop()
    return result["update_now"]


# ================== 启动时更新检查 ===================
def check_and_update_on_startup() -> bool:
    """启动时检查更新并处理"""
    try:
        current_ver = get_current_version()
        remote_ver, download_url, changelog, exe_sha256, remote_arch = get_remote_version()

        # 校验远程信息完整性
        if not all([remote_ver, download_url, exe_sha256, remote_arch]):
            logger.warning("远程版本信息不完整，跳过更新")
            return True

        # 版本号对比
        current_parsed = parse_version(current_ver)
        remote_parsed = parse_version(remote_ver)
        logger.info(f"[启动检查] 版本对比：当前 {current_parsed} vs 远程 {remote_parsed}")

        if remote_parsed > current_parsed:
            logger.info(f"检测到新版本 {remote_ver}，弹出更新提示")
            should_update = show_update_dialog(current_ver, remote_ver, changelog)
            if should_update:
                logger.info("用户选择立即更新，启动更新流程")
                logger.info(f"[更新核心] 开始执行perform_update_flow")
                perform_update_flow(current_ver, remote_ver, download_url, changelog, exe_sha256, remote_arch)
                return False
            else:
                logger.info("用户选择稍后更新")
                return True
        else:
            logger.info("已是最新版本")
            return True

    except Exception as e:
        logger.error(f"启动更新检查异常: {e}", exc_info=True)
        return True


def exit_and_launch_updater(
    extracted_exe: Path,
    current_executable: Path,
    extracted_ver_file: Path,
    expected_sha256: str
):
    """主程序主动退出，并在退出后启动 updater（确保资源释放）"""
    logger.info("[退出流程] 主程序准备主动退出，释放所有资源...")

    # 第一次：强制终止其他同名实例（防止文件被锁）
    logger.info(f"[退出流程] 当前进程 PID: {os.getpid()}---{CURRENT_EXE.name}")
    kill_process_by_name(CURRENT_EXE.name)

    # 关闭所有子线程（最多等待 2 秒）
    for thread in threading.enumerate():
        if thread != threading.main_thread():
            try:
                thread.join(timeout=2.0)
                logger.info(f"[退出流程] 已等待线程 {thread.name} 退出")
            except Exception as e:
                logger.warning(f"[退出流程] 线程 {thread.name} join 异常: {e}")

    # 清理 PyInstaller 临时目录（可选但推荐）
    clean_old_mei_tempdirs()

    # 等待 2 秒，确保系统释放文件句柄
    time.sleep(2)

    # 第二次：再次 kill 同名进程（双重保险）
    logger.info(f"[退出流程] 当前进程 PID: {os.getpid()}---{CURRENT_EXE.name}")
    kill_process_by_name(CURRENT_EXE.name)
    time.sleep(1)  # 再等 1 秒确保完全释放

    # 构建 updater 启动命令
    try:
        if getattr(sys, 'frozen', False):
            updater_executable = APP_ROOT / "updater.exe"
            if not updater_executable.exists():
                raise FileNotFoundError(f"未找到 updater.exe: {updater_executable}")
            cmd = [
                f'"{updater_executable}"',
                f'"{extracted_exe}"',
                f'"{current_executable}"',
                expected_sha256,
                f'"{extracted_ver_file}"'
            ]
        else:
            updater_script = APP_ROOT / "updater.py"
            if not updater_script.exists():
                raise FileNotFoundError(f"未找到 updater.py: {updater_script}")
            cmd = [
                f'"{sys.executable}"',
                f'"{updater_script}"',
                f'"{extracted_exe}"',
                f'"{current_executable}"',
                expected_sha256,
                f'"{extracted_ver_file}"'
            ]

        cmd_str = ' '.join(cmd)
        logger.info(f"[退出流程] 启动 updater 命令: {cmd_str}")

        # 使用 DETACHED_PROCESS 避免继承句柄，确保独立运行
        subprocess.Popen(
            cmd_str,
            shell=True,
            cwd=str(APP_ROOT),
            close_fds=True,
            creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
        )

    except Exception as e:
        logger.error(f"[退出流程] 启动 updater 失败: {e}", exc_info=True)
        messagebox.showerror("更新错误", f"无法启动更新器：{e}")
        # 即使失败也强制退出
        os._exit(1)

    # 彻底退出当前进程（绕过 sys.exit，防止 tkinter 或其他模块阻塞）
    logger.info("[退出流程] 主程序即将强制退出...")
    os._exit(0)


# ================== 主程序 GUI ==================
def run_main_gui():
    """运行主程序GUI"""
    try:
        clean_old_mei_tempdirs()
        app = ctk.CTk()
        app.title("电商数据采集工具")
        app.geometry("600x400")
        app.resizable(False, False)
        logger.info("[GUI] 主界面启动")

        title = ctk.CTkLabel(app, text="电商数据采集工具", font=("Microsoft YaHei", 20, "bold"))
        title.pack(pady=25)
        version_label = ctk.CTkLabel(app, text=f"当前版本：{get_current_version()}")
        version_label.pack()

        status_label = ctk.CTkLabel(app, text="状态：就绪", text_color="green")
        status_label.pack(pady=15)

        def start_crawling():
            status_label.configure(text="状态：爬取中...", text_color="blue")
            logger.info("用户点击【开始爬取】")
            status_label.after(2000, lambda: status_label.configure(text="状态：完成", text_color="green"))

        ctk.CTkButton(app, text="开始采集", command=start_crawling, width=150, height=40,
                      font=("Microsoft YaHei", 14)).pack(pady=20)

        def on_close():
            logger.info("用户关闭程序，执行清理...")
            clean_old_mei_tempdirs()
            app.destroy()
            logger.info("[GUI] 主界面关闭")

        app.protocol("WM_DELETE_WINDOW", on_close)
        app.mainloop()

    except Exception as e:
        logger.critical("GUI 运行崩溃", exc_info=True)
        messagebox.showerror("严重错误", "程序发生未预期错误，请查看 logs/app.log")
    finally:
        clean_old_mei_tempdirs()


# ================== 程序入口 ==================
if __name__ == '__main__':
    # 判断是否为更新器模式（由主程序调用时传入参数）
    if len(sys.argv) < 5 and "updater" in sys.argv[0].lower():
        # 更新器模式：执行文件替换逻辑
        source_exe = Path(sys.argv[1])
        target_exe = Path(sys.argv[2])
        expected_sha256 = sys.argv[3]
        version_file = Path(sys.argv[4])
        replace_files_and_restart(source_exe, target_exe, version_file)
    else:
        # 主程序模式：正常启动
        try:
            ctypes.windll.shcore.SetProcessDpiAwareness(1)
            logger.info("[入口] 已设置DPI感知")
        except:
            logger.warning("[入口] 无法设置DPI感知，可能影响显示效果")
            pass

        ctk.set_appearance_mode("system")
        ctk.set_default_color_theme("blue")
        logger.info("[入口] 初始化UI主题完成")

        logger.info("========== 程序启动 ==========")
        logger.info(f"工作目录: {APP_ROOT}")
        logger.info(f"当前可执行文件: {CURRENT_EXE}")

        if check_and_update_on_startup():
            run_main_gui()
        else:
            logger.info("更新流程已接管，主程序无需启动")

        logger.info("========== 程序结束 ==========")